Implementing ViewModel and Lifecycle-Aware Components
Course: Android App Development
Topic: Implementing ViewModel and Lifecycle-Aware Components
Course Link: Android App Development Course
Demo Link: Register for a Course Demo
Website: JustAcademy
1. Introduction
Modern Android applications must manage screen data, user interactions, background operations, and changes in the Activity or Fragment lifecycle. When all application logic is placed directly inside an Activity or Fragment, the code can become difficult to maintain and may lose important state during configuration changes such as screen rotation.
Android Jetpack provides architecture components such as ViewModel, Lifecycle, LiveData, and coroutine lifecycle scopes to help developers organize application logic and manage UI updates safely.
A ViewModel stores and manages screen-related state independently of a particular Activity or Fragment instance. Lifecycle-aware components observe the lifecycle of their owner and adjust their behavior accordingly. Together, these components help applications avoid unnecessary work, reduce memory-leak risks, and deliver a more consistent user experience.
Learning Objectives
- Understand the purpose of ViewModel in Android development.
- Learn how ViewModel survives configuration changes.
- Understand Activity, Fragment, and ViewModel lifecycles.
- Implement ViewModel using Kotlin.
- Manage UI state with StateFlow and LiveData.
- Use lifecycle-aware coroutine scopes.
- Collect Flow safely with repeatOnLifecycle.
- Separate UI logic, business logic, and data access.
- Handle loading, success, and error states.
- Test ViewModels and apply lifecycle best practices.
2. What Is a ViewModel?
ViewModel is an Android Architecture Component designed to store and manage UI-related data in a lifecycle-conscious way. It separates screen state and related logic from the Activity or Fragment that displays the screen.
For example, consider a shopping application that displays a product list. The ViewModel can maintain the products, search query, selected category, and loading status. The Activity or Fragment observes this information and renders the appropriate interface.
Responsibilities of a ViewModel
- Maintain screen-related state.
- Handle user actions received from the UI.
- Coordinate with repositories or domain-layer components.
- Expose observable data such as StateFlow or LiveData.
- Launch coroutine operations using viewModelScope.
- Prepare data for presentation without directly manipulating Android views.
What a ViewModel Should Avoid
- Holding references to an Activity, Fragment, View, or Activity context.
- Directly modifying TextViews, buttons, or other UI widgets.
- Containing every database and networking implementation itself.
- Depending on a particular screen instance remaining alive.
A ViewModel should normally coordinate data operations through repositories rather than directly performing all data-access work. This separation improves testability and makes application code easier to maintain.
3. Why Is ViewModel Important?
Android can destroy and recreate an Activity or Fragment during configuration changes. If important screen state exists only in the UI controller, that state may be lost and the application may repeat unnecessary operations.
A ViewModel is retained by its ViewModelStoreOwner across configuration changes. When the screen is recreated, the new UI controller can obtain the existing ViewModel and continue observing its state.
| Without ViewModel |
With ViewModel |
| Screen data may be stored directly in the Activity. |
Screen data is managed separately from the Activity instance. |
| Rotation may require repeating data preparation. |
In-memory ViewModel state can survive configuration changes. |
| UI controllers may contain too much business logic. |
Responsibilities can be separated between UI, ViewModel, and repository. |
| Logic can be difficult to test independently. |
ViewModel logic can be tested without rendering the full UI. |
| Background tasks may be tied incorrectly to a screen instance. |
ViewModel-owned coroutines can follow the ViewModel's lifetime. |
Important: ViewModel does not automatically persist data after the process is killed. Use SavedStateHandle for appropriate small pieces of restorable UI state, and use persistent storage such as Room or DataStore for data that must survive process death or app restarts.
4. Understanding the Android Lifecycle
The lifecycle represents the state of an Android component as it is created, becomes visible, interacts with the user, moves into the background, and is eventually destroyed.
Activities and Fragments have lifecycle callbacks that notify the application about these changes. Lifecycle-aware components can observe these states instead of requiring every component to manually coordinate all lifecycle events.
Common Activity Lifecycle Methods
| Method |
Purpose |
onCreate() |
Initializes the Activity, its UI, and required objects. |
onStart() |
The Activity becomes visible to the user. |
onResume() |
The Activity enters the foreground interaction state. |
onPause() |
The Activity loses its fully interactive foreground status. |
onStop() |
The Activity is no longer visible. |
onDestroy() |
The Activity instance is being destroyed. |
Typical Lifecycle Sequence
onCreate()
|
onStart()
|
onResume()
|
Activity is active
|
onPause()
|
onStop()
|
onDestroy() when the instance is destroyed
This is a simplified sequence. An Activity may pause and resume repeatedly, and it may be stopped and restarted without being destroyed. Lifecycle behavior also depends on navigation, system events, and the state of other visible components.
5. Understanding the ViewModel Lifecycle
The ViewModel lifecycle is associated with a ViewModelStoreOwner, such as an Activity, Fragment, or navigation destination. It is not identical to the lifecycle of a particular Activity or Fragment instance.
What Happens During Screen Rotation?
- The existing Activity instance is destroyed as part of the configuration change.
- The ViewModel associated with the Activity's ViewModelStore is retained.
- Android creates a new Activity instance.
- The new Activity obtains the ViewModel from the same retained store.
- The new Activity renders the state already held by the ViewModel.
The ViewModel is cleared when its owning scope is permanently removed. For example, an Activity-scoped ViewModel is normally cleared when its Activity finishes permanently, while a navigation-entry-scoped ViewModel is cleared when that entry is removed from the back stack.
ViewModel Lifecycle Example
class CounterViewModel : ViewModel() {
var count: Int = 0
private set
fun increment() {
count++
}
}
If an Activity obtains this ViewModel and increments the counter, the in-memory count can survive an ordinary configuration change. A production UI should generally expose observable state, such as StateFlow, so that the recreated UI can render updates automatically.
6. Creating a ViewModel in Kotlin
To create a ViewModel, define a class that extends ViewModel. Store screen-related information in the class and provide functions that handle user actions.
Basic Counter ViewModel
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.value += 1
}
fun decrement() {
_count.value -= 1
}
fun reset() {
_count.value = 0
}
}
In this implementation:
CounterViewModel extends the Android ViewModel class.
MutableStateFlow stores the current counter value.
_count is private and mutable inside the ViewModel.
count exposes a read-only StateFlow to the UI.
- The public functions provide controlled ways to update the state.
Using a read-only public state stream prevents the UI from directly modifying the ViewModel's internal state.
7. Obtaining a ViewModel in an Activity
Android provides lifecycle-aware ViewModel providers. In a typical Kotlin Activity, the by viewModels() property delegate obtains a ViewModel scoped to that Activity.
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
class CounterActivity : AppCompatActivity() {
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set up the UI and connect it to viewModel.
}
}
The Activity's ViewModel instance is obtained through the Activity's ViewModel store. The delegate can return the existing ViewModel after a configuration change rather than creating a new instance unnecessarily.
For a Fragment, use the Fragment's ViewModel delegate or another supported ViewModel provider. The selected owner determines how long the ViewModel is retained.
8. ViewModel Scopes: Activity, Fragment, and Navigation
A ViewModel can be scoped to different owners depending on the state it manages. Selecting the right owner helps determine which screens share state and when that state should be cleared.
| Scope |
Typical Use |
Lifetime |
| Activity |
State shared by multiple Fragments in one Activity. |
Until the Activity's ViewModel store is permanently cleared. |
| Fragment |
State used by one Fragment screen. |
Until that Fragment's ViewModel store is permanently cleared. |
| Navigation destination |
State belonging to a particular navigation entry. |
Until the destination is removed from its navigation back stack. |
| Navigation graph |
State shared by several screens within a navigation flow. |
Until the graph's scoped owner is removed. |
Sharing a ViewModel Between Fragments
import androidx.fragment.app.activityViewModels
class ProductListFragment : Fragment() {
private val viewModel: SharedProductViewModel by activityViewModels()
}
class ProductDetailsFragment : Fragment() {
private val viewModel: SharedProductViewModel by activityViewModels()
}
Both Fragments obtain a ViewModel scoped to their shared Activity. This can be useful when selecting a product in one Fragment should update the details shown by another Fragment.
For a ViewModel that should belong only to one Fragment, use a Fragment-scoped delegate such as by viewModels() instead.
9. Managing UI State with StateFlow
StateFlow is a hot stream that stores a current value and emits state updates to active collectors. It is well suited to screen state because the UI needs to know the latest information when it starts observing.
For example, a product screen may need to represent loading, successfully loaded products, and a failure message. A single state model can represent these mutually exclusive conditions.
Defining UI State
data class Product(
val id: Int,
val name: String,
val price: Double
)
sealed interface ProductUiState {
data object Loading : ProductUiState
data class Success(
val products: List<Product>
) : ProductUiState
data class Error(
val message: String
) : ProductUiState
}
A sealed interface makes the possible UI states explicit. The UI can handle each state separately and avoid displaying contradictory combinations such as an error and a success result at the same time.
ViewModel with UI State
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class ProductViewModel(
private val repository: ProductRepository
) : ViewModel() {
private val _uiState =
MutableStateFlow<ProductUiState>(ProductUiState.Loading)
val uiState: StateFlow<ProductUiState> =
_uiState.asStateFlow()
fun loadProducts() {
viewModelScope.launch {
_uiState.value = ProductUiState.Loading
try {
val products = repository.getProducts()
_uiState.value = ProductUiState.Success(products)
} catch (exception: CancellationException) {
throw exception
} catch (exception: Exception) {
_uiState.value = ProductUiState.Error(
exception.message ?: "Unable to load products"
)
}
}
}
}
This example assumes that ProductRepository.getProducts() is a suspending function that returns a list of products. The ViewModel starts the operation in viewModelScope and updates the state according to the result.
Note: The example rethrows CancellationException to preserve structured concurrency. In a larger application, consider representing errors with user-friendly messages and logging diagnostic details separately.
10. What Are Lifecycle-Aware Components?
Lifecycle-aware components are Android components or APIs that can observe the lifecycle state of an Activity, Fragment, or another LifecycleOwner and adjust their behavior accordingly.
Without lifecycle awareness, a component may continue observing data or performing work when its UI is stopped or destroyed. This can waste resources and create opportunities for invalid UI updates or memory leaks.
Common Lifecycle-Aware Components
| Component |
Purpose |
Lifecycle |
Represents lifecycle states and events. |
LifecycleOwner |
Exposes a Lifecycle that other components can observe. |
ViewModel |
Manages screen state and logic across configuration changes. |
LiveData |
Provides observable data that respects the lifecycle of its observer. |
lifecycleScope |
Runs coroutines that are cancelled when the associated Lifecycle is destroyed. |
viewModelScope |
Runs coroutines that are cancelled when the ViewModel is cleared. |
repeatOnLifecycle |
Starts a coroutine block at a chosen lifecycle state and cancels it when the lifecycle falls below that state. |
11. Understanding LifecycleOwner
LifecycleOwner is an interface implemented by classes that expose a Lifecycle. Common examples include AndroidX Activities and Fragments.
A LifecycleOwner allows observers and lifecycle-aware operations to determine whether the component is created, started, resumed, or destroyed.
Example
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
fun isScreenVisible(owner: LifecycleOwner): Boolean {
return owner.lifecycle.currentState
.isAtLeast(Lifecycle.State.STARTED)
}
This function checks whether the owner's current state is at least STARTED. It is a state check, not a guarantee that every possible UI operation is safe; UI updates must still follow the appropriate lifecycle and threading rules.
12. Using LiveData with ViewModel
LiveData is an observable data holder that is aware of the lifecycle of its observers. When observed with an Activity or Fragment as the LifecycleOwner, it generally delivers updates only while the owner is active, and it removes the observer when the owner is destroyed.
LiveData remains useful in existing Android applications and projects that already use it extensively. StateFlow is also widely used for Kotlin-first applications, particularly when working with Coroutines and Flow.
ViewModel with LiveData
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class UsernameViewModel : ViewModel() {
private val _username = MutableLiveData("Guest")
val username: LiveData<String> = _username
fun updateUsername(newName: String) {
_username.value = newName
}
}
Observing LiveData in an Activity
class UsernameActivity : AppCompatActivity() {
private val viewModel: UsernameViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.username.observe(this) { name ->
// Update the username TextView.
// usernameTextView.text = name
}
}
}
The observer uses the Activity as its LifecycleOwner. The actual TextView and layout setup must be implemented by the application.
13. Choosing Between LiveData and StateFlow
| Feature |
LiveData |
StateFlow |
| Library |
AndroidX Lifecycle. |
Kotlin Coroutines Flow. |
| Initial value |
Can be absent when using certain LiveData types. |
Always requires an initial value. |
| Lifecycle behavior |
Observations respect an attached LifecycleOwner. |
Collection requires lifecycle-aware collection when used for UI. |
| Coroutine operators |
Does not directly provide the full Flow operator API. |
Supports Flow operators and coroutine integration. |
| Common use |
Existing lifecycle-based observable UI data. |
State management in Kotlin and coroutine-based architecture. |
Neither type eliminates the need for sound state modeling. Choose the approach that best fits the project architecture, dependencies, and existing codebase.
14. Using viewModelScope
viewModelScope is a coroutine scope associated with a ViewModel. Coroutines launched inside it are automatically cancelled when that ViewModel is cleared.
This makes it suitable for loading data, coordinating repository calls, and performing operations whose lifetime should be tied to a screen's ViewModel rather than a specific Activity instance.
Example
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
class MessageViewModel : ViewModel() {
fun loadMessage() {
viewModelScope.launch {
delay(1000)
// Update observable state after the operation.
}
}
}
The delay is illustrative. Real applications should update an observable state property after a repository or domain operation completes.
15. Using lifecycleScope
lifecycleScope provides a coroutine scope tied to a Lifecycle. Coroutines launched in it are cancelled when that Lifecycle is destroyed.
In an Activity, the Activity lifecycle scope can be used for UI-related asynchronous work whose lifetime belongs to that Activity. In a Fragment, use the view lifecycle scope when the operation is tied to the Fragment's current view.
Fragment Example
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
class ProfileFragment : Fragment() {
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
// Perform an operation tied to this Fragment view.
}
}
}
The imports for View and Bundle should be included in the actual source file. Using viewLifecycleOwner.lifecycleScope is especially important when work interacts with view objects because a Fragment's view may be destroyed before the Fragment itself.
16. Collecting Flow with repeatOnLifecycle
When a ViewModel exposes a Flow or StateFlow, the UI needs to collect it without unnecessarily running collection while the screen is stopped. The recommended pattern for Views-based Android UI is to use repeatOnLifecycle().
The block starts when the lifecycle reaches the chosen state, commonly STARTED. It is cancelled when the lifecycle falls below that state and starts again when the lifecycle returns to the selected state.
Fragment Example
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import kotlinx.coroutines.launch
class ProductFragment : Fragment() {
private val viewModel: ProductViewModel by viewModels()
override fun onViewCreated(
view: View,
savedInstanceState: Bundle?
) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(
Lifecycle.State.STARTED
) {
viewModel.uiState.collect { state ->
when (state) {
is ProductUiState.Loading -> {
// Show loading indicator.
}
is ProductUiState.Success -> {
// Render products.
}
is ProductUiState.Error -> {
// Display an error message.
}
}
}
}
}
}
}
The code demonstrates the collection pattern. It assumes that ProductViewModel and ProductUiState are defined as in the previous sections. Implement the rendering logic for the actual layout.
Why Use repeatOnLifecycle?
- Stops collection while the UI is below the selected lifecycle state.
- Restarts collection when the UI becomes active again.
- Reduces unnecessary processing while the screen is stopped.
- Helps prevent UI updates from continuing in an inactive screen.
- Works naturally with StateFlow and other Flow-based data sources.
For multiple Flows, launch a separate child coroutine for each collection inside the repeatOnLifecycle block. Otherwise, the first non-terminating collect call can prevent subsequent collection statements from starting.
17. Collecting Multiple Flows Safely
Suppose a screen displays both a user's profile and their notification count. These values may be exposed as separate StateFlow properties. Each stream should be collected in its own coroutine when both are needed concurrently.
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(
Lifecycle.State.STARTED
) {
launch {
viewModel.profile.collect { profile ->
// Render profile information.
}
}
launch {
viewModel.notificationCount.collect { count ->
// Render the notification count.
}
}
}
}
This approach allows both streams to be collected concurrently while the screen is STARTED. When the lifecycle falls below STARTED, the repeat block and its child collections are cancelled.
18. Preserving Small UI State with SavedStateHandle
ViewModel can retain in-memory state across configuration changes, but it does not guarantee survival after process death. SavedStateHandle can preserve small pieces of UI state, such as a search query, selected item ID, or navigation-related input, so the ViewModel can restore them when it is recreated.
Example: Saving a Search Query
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.StateFlow
class SearchViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
companion object {
private const val QUERY_KEY = "search_query"
}
val query: StateFlow<String> =
savedStateHandle.getStateFlow(QUERY_KEY, "")
fun updateQuery(value: String) {
savedStateHandle[QUERY_KEY] = value
}
}
The SavedStateHandle is normally supplied through a supported ViewModel creation mechanism. Its saved state is intended for small, serializable state values rather than large objects or complete database records.
19. Understanding LifecycleObserver
A lifecycle observer allows a class to respond to lifecycle events without requiring all related logic to be placed in Activity or Fragment callbacks. AndroidX provides DefaultLifecycleObserver for observing lifecycle events such as start, resume, pause, and stop.
Example
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
class ScreenLogger : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
// Start lifecycle-related monitoring.
}
override fun onStop(owner: LifecycleOwner) {
// Stop lifecycle-related monitoring.
}
}
Register the observer with an appropriate LifecycleOwner:
lifecycle.addObserver(ScreenLogger())
For this example, retain the observer instance if you need to remove it explicitly or interact with it later. Lifecycle observers are useful for reusable behavior, but do not add observers unnecessarily when a built-in lifecycle-aware API already handles the requirement.
20. Separating UI, ViewModel, and Repository Layers
A maintainable Android application separates the display of information from state management and data access. The UI sends user actions to the ViewModel. The ViewModel coordinates application logic and requests data from a repository. The repository obtains data from a database, network service, or another source.
Architecture Overview
User interaction
|
v
Activity / Fragment
|
| sends action
v
ViewModel
|
| requests data
v
Repository
|
+------------------+
| |
v v
Remote API Local Database
| |
+---------+--------+
|
v
Repository result
|
v
ViewModel state
|
v
Lifecycle-aware UI
Responsibilities by Layer
| Layer |
Main Responsibility |
| Activity or Fragment |
Displays UI and forwards user interactions. |
| ViewModel |
Manages screen state and coordinates actions. |
| Repository |
Provides a consistent interface to application data. |
| Remote data source |
Communicates with web services or APIs. |
| Local data source |
Reads or writes data in local storage. |
This separation prevents UI controllers from becoming responsible for every networking, persistence, and state-management detail.
21. Complete Practical Example: Loading User Profiles
Project Goal: Implement a screen that loads a user profile, displays a loading indicator while the operation is running, and shows either the profile or an error message.
Step 1: Create the Data Model
data class UserProfile(
val id: Int,
val name: String,
val email: String
)
Step 2: Define a Repository
import kotlinx.coroutines.delay
class UserRepository {
suspend fun getUserProfile(): UserProfile {
delay(1000)
return UserProfile(
id = 1,
name = "Aarav Sharma",
email = "[email protected]"
)
}
}
This repository uses a delay to simulate an asynchronous operation. In a production application, replace it with a real data source and appropriate error handling.
Step 3: Define Screen States
sealed interface UserProfileUiState {
data object Loading : UserProfileUiState
data class Success(
val profile: UserProfile
) : UserProfileUiState
data class Error(
val message: String
) : UserProfileUiState
}
Step 4: Implement the ViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class UserProfileViewModel(
private val repository: UserRepository
) : ViewModel() {
private val _uiState =
MutableStateFlow<UserProfileUiState>(
UserProfileUiState.Loading
)
val uiState: StateFlow<UserProfileUiState> =
_uiState.asStateFlow()
init {
loadProfile()
}
fun loadProfile() {
viewModelScope.launch {
_uiState.value = UserProfileUiState.Loading
try {
val profile = repository.getUserProfile()
_uiState.value =
UserProfileUiState.Success(profile)
} catch (exception: CancellationException) {
throw exception
} catch (exception: Exception) {
_uiState.value = UserProfileUiState.Error(
exception.message ?: "Could not load profile"
)
}
}
}
}
The ViewModel exposes observable state and delegates data retrieval to the repository. The ViewModel's coroutine is cancelled when the ViewModel is cleared.
Step 5: Collect the State in a Fragment
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(
Lifecycle.State.STARTED
) {
viewModel.uiState.collect { state ->
when (state) {
is UserProfileUiState.Loading -> {
// Show progress indicator.
}
is UserProfileUiState.Success -> {
// Display state.profile.name and email.
}
is UserProfileUiState.Error -> {
// Display state.message.
}
}
}
}
}
Place this collection inside the Fragment's onViewCreated() method after the view has been created. The ViewModel must be obtained using a compatible factory or dependency-injection setup because this example has a repository constructor parameter.
Expected Result
- The screen initially displays a loading indicator.
- The repository simulates a one-second data operation.
- The ViewModel updates the state to Success when data is available.
- The UI displays the profile information.
- If the operation fails, the ViewModel exposes an Error state.
- During a configuration change, the ViewModel can retain its current state and continue eligible work.
22. Testing ViewModels
ViewModels should be testable independently of the Activity or Fragment. A common approach is to provide a fake repository and verify that the ViewModel exposes the expected state after a user action or data operation.
Example Test Concept
// Illustrative test outline:
//
// 1. Create a fake repository that returns known data.
// 2. Create the ViewModel using that repository.
// 3. Trigger loadProfile().
// 4. Advance or wait for the coroutine work as needed.
// 5. Assert that uiState contains Success with the expected profile.
For actual unit tests involving viewModelScope, configure a test Main dispatcher where required and use coroutine test utilities such as runTest. When testing StateFlow, collect or inspect its current value according to the behavior being tested.
What Should Be Tested?
- Initial UI state.
- Successful repository responses.
- Repository failures and error messages.
- User actions that change state.
- Loading state transitions.
- Cancellation behavior where relevant.
- Restoration of state through SavedStateHandle when applicable.
23. Common Mistakes and Troubleshooting
| Mistake |
Possible Problem |
Recommended Solution |
| Storing Activity references in ViewModel |
May cause memory leaks when the Activity is recreated. |
Keep ViewModel independent of UI lifecycle-bound objects. |
| Expecting ViewModel to survive process death automatically |
In-memory state can be lost when the process is terminated. |
Use SavedStateHandle or persistent storage as appropriate. |
| Collecting Flow without lifecycle awareness |
Work may continue while the screen is stopped. |
Use repeatOnLifecycle for Views-based UI collection. |
| Using Fragment lifecycle instead of view lifecycle for view-bound collection |
Collection may outlive the Fragment's view. |
Use viewLifecycleOwner for UI-related collection. |
| Putting network and database code directly in Activity |
UI controller becomes difficult to test and maintain. |
Move data access into repository and data-source layers. |
| Making internal state publicly mutable |
Any UI consumer may change the state unexpectedly. |
Expose read-only StateFlow or LiveData. |
| Swallowing CancellationException |
Cancellation may stop behaving as expected. |
Rethrow cancellation exceptions. |
| Assuming every coroutine should use lifecycleScope |
Work may be cancelled too early or tied to the wrong owner. |
Choose the scope according to the lifetime of the operation. |
| Creating a ViewModel with constructor dependencies but no factory |
ViewModel creation can fail at runtime. |
Use a compatible ViewModelProvider.Factory or dependency injection. |
| Updating views from a background thread |
Can cause threading errors or unstable UI behavior. |
Perform UI updates on the main thread. |
24. Best Practices
- Use ViewModel for screen-level state and related UI logic.
- Keep Activity and Fragment classes focused on rendering and forwarding user actions.
- Expose StateFlow or LiveData as read-only state.
- Use repositories to separate data retrieval from UI logic.
- Use viewModelScope for work that belongs to the ViewModel's lifetime.
- Use lifecycleScope for work that belongs to a specific Lifecycle.
- Use viewLifecycleOwner for Fragment view-bound operations.
- Prefer repeatOnLifecycle for lifecycle-aware Flow collection in Views-based interfaces.
- Represent loading, success, and failure explicitly.
- Use SavedStateHandle for appropriate small UI values that need restoration.
- Use Room, DataStore, or another persistent store for durable application data.
- Avoid keeping Activity, Fragment, or View references inside ViewModels.
- Use dependency injection or ViewModel factories for constructor dependencies.
- Write unit tests for state changes and error-handling behavior.
25. Practical Exercises
- Create a CounterViewModel with increment, decrement, and reset functions.
- Expose the counter through StateFlow and observe it from an Activity.
- Rotate the device and verify that the counter's in-memory state is retained.
- Create a UserProfileViewModel that exposes Loading, Success, and Error states.
- Replace a simulated repository with a real API or local database.
- Collect a ViewModel Flow using repeatOnLifecycle in a Fragment.
- Share a ViewModel between two Fragments using an Activity-scoped ViewModel.
- Use SavedStateHandle to restore a search query.
- Write unit tests for successful and failed repository operations.
- Inspect an existing Activity and move suitable screen logic into a ViewModel.
26. Interview Questions and Answers
Q1. What is a ViewModel in Android?
A ViewModel is a lifecycle-conscious component that stores and manages UI-related state and logic separately from an Activity or Fragment instance.
Q2. Why is ViewModel used?
It helps separate UI logic, retain in-memory state across configuration changes, coordinate data operations, and improve testability.
Q3. Does ViewModel survive screen rotation?
Normally, yes. A ViewModel is retained across configuration changes while its ViewModelStoreOwner remains in the same retained scope.
Q4. Does ViewModel survive process death?
No. In-memory ViewModel state is not guaranteed to survive process death. Use SavedStateHandle for suitable restorable UI state and persistent storage for durable data.
Q5. What is LifecycleOwner?
LifecycleOwner is an interface that exposes a Lifecycle, allowing components to observe the lifecycle state of an Activity, Fragment, or another supported owner.
Q6. What is the difference between viewModelScope and lifecycleScope?
viewModelScope is cancelled when the ViewModel is cleared. lifecycleScope is cancelled when its associated Lifecycle is destroyed.
Q7. What is repeatOnLifecycle?
It launches a coroutine block when a Lifecycle reaches a selected state and cancels the block when the Lifecycle falls below that state. It restarts the block when the Lifecycle becomes active again.
Q8. What is the difference between LiveData and StateFlow?
LiveData is an AndroidX observable data holder with lifecycle-aware observers. StateFlow is a Kotlin Flow type that always holds a current value and is commonly collected with lifecycle-aware APIs in Android UI.
Q9. Why should a ViewModel avoid holding an Activity reference?
A ViewModel may outlive a particular Activity instance during configuration changes. Holding that instance can retain obsolete UI objects and cause memory leaks.
Q10. What is SavedStateHandle?
SavedStateHandle stores suitable small state values that can be restored when a ViewModel is recreated after supported system-driven process recreation.
Q11. Why use a repository with ViewModel?
A repository separates data access from screen state management and can coordinate data from network services, databases, or other sources.
Q12. How should multiple Flows be collected in a Fragment?
Use viewLifecycleOwner.lifecycleScope with repeatOnLifecycle and launch a separate child coroutine for each Flow collection.
27. Quick Revision Checklist
- ViewModel stores screen-related state and logic.
- ViewModel state normally survives configuration changes.
- In-memory ViewModel data is not permanent storage.
- LifecycleOwner exposes the lifecycle of a component.
- LiveData supports lifecycle-aware observation.
- StateFlow stores the current state and emits updates.
- viewModelScope is cancelled when the ViewModel is cleared.
- lifecycleScope is cancelled when its Lifecycle is destroyed.
- repeatOnLifecycle supports lifecycle-aware coroutine collection.
- Use viewLifecycleOwner for Fragment view-bound collection.
- Repositories separate data access from UI logic.
- SavedStateHandle can restore suitable small UI state values.
- Keep ViewModels independent of Activity and Fragment references.
28. Conclusion
Implementing ViewModel and lifecycle-aware components is an important step toward building reliable Android applications. ViewModel separates screen state from UI controllers, while LifecycleOwner, LiveData, StateFlow, lifecycleScope, viewModelScope, and repeatOnLifecycle help coordinate updates and asynchronous work with the appropriate lifetime.
By combining these components with repository-based architecture, explicit UI states, and appropriate testing, developers can create Android applications that are easier to maintain, more resilient to configuration changes, and less likely to perform unnecessary work when screens are inactive.
29. Learning Resources